iT邦幫忙

2023 iThome 鐵人賽

DAY 11
3
AI & Data

LLM 學習筆記系列 第 11

LLM Note Day 11 - 擁抱開源的微笑 Hugging Face Transformers

  • 分享至 

  • xImage
  •  

簡介

Hugging Face 🤗 Transformers 是訓練 Transformer 模型最知名的套件沒有之一,此套件收入了許多知名模型架構、訓練演算法以及各種模型權重的分享,使開發者可以輕鬆駕馭這些理論複雜的操作。

Transformers 是個著重在訓練的框架,但也支援了相當豐富的推理功能,今天將會著重在如何使用此框架進行 LLM 的推論,並剖析 Transformer Decoder 進行 Autoregressive 的流程。

可愛貓貓 Day 11

(Powered By Microsoft Designer)

安裝

Transformers 支援的後端 ML 框架包含 PyTorch, Tensorflow, JAX 等等,筆者是以 PyTorch 為主:

conda install pytorch pytorch-cuda=11.8 -c pytorch -c nvidia
pip install transformers

使用 Transformers 套件時,除了套件本身,還有許多其他附加的套件可以選用,能夠進一步提升運作速度。

pip install accelerate bitsandbytes

這兩個套件能夠讓我們使用量化 (Quantization) 機制來減少 GPU 記憶體消耗。

讀取模型

以下程式碼可在 Colab 上嘗試,比較不用煩惱模型權重很佔硬碟空間或者要下載很久的問題,完整的程式碼可以參考此 Colab 筆記本

Transformers 讀取模型的方式有很多種,最常見的是使用 .from_pretrained 讀取,這裡使用 TheBloke 大神提供的 Llama-2 7B Chat 為例:

from transformers import LlamaForCausalLM as ModelCls

model: ModelCls = ModelCls.from_pretrained(
    "TheBloke/Llama-2-7b-chat-fp16",
    device_map="auto",
    load_in_8bit=True,  # 約需 8 GiB GPU 記憶體
    # load_in_4bit=True,  # 約需 6 GiB GPU 記憶體
    low_cpu_mem_usage=True
)

TheBloke 是 Hugging Face 社群相當活躍的一個用戶,每次有很猛的模型釋出時,他就會手刀衝第一個幫大家把模型的格式轉換成 HF 支援的格式,進行各種量化壓縮後上傳。據說他的網路環境達數 GB/s 之快,是個非常熱心的大神。

在像是 Colab 這種資源有限的機器上,需要使用 8 位元量化 (load_in_8bit) 或 4 位元量化 (load_in_4bit) 來減少 GPU 記憶體的消耗。為了啟用量化功能,需要將 device_map 設定為 "auto",以確保模型權重都放在 GPU 裡面。如果 CPU 記憶體也不是很夠的話,那就需要將 low_cpu_mem_usage 設為 True 才能比較順利的讀取模型。

這個步驟實際上會從 Hugging Face 提供的模型儲存空間下載權重,預設情況下會放在 ~/.cache/huggingface/ 裡面,也可以透過 cache_dir 參數指定放在哪裡:

model: ModelCls = ModelCls.from_pretrained(
    model_path,
    cache_dir="./cache_models"
)

其他的模型可以在 Hugging Face Hub 上找到。Hugging Face Hub 主要是使用 Git 做管理的,所以你也能直接使用 git clone 將模型複製下來,在此之前需要先安裝 Git LFS 模組:

sudo apt install -y git git-lfs

啟用 Git LFS 之後再進行下載:

git lfs install  # 啟用 Git LFS 功能
git clone https://huggingface.co/TheBloke/Llama-2-7b-chat-fp16

完成下載後,直接指定資料夾路徑名稱即可讀取該模型:

ModelCls.from_pretrained("Llama-2-7b-chat-fp16")

Git 在傳輸大型檔案時並不會顯示下載進度,所以只能慢慢等,過一陣子之後模型就會下載完成了!但如果你真的很想要看那個酷酷的網速進度條,其實也是有辦法的:

# 跳過所有 LFS 檔案的下載
GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/TheBloke/Llama-2-7b-chat-fp16
cd Llama-2-7b-chat-fp16

# 透過 Git LFS 進行 Fetch
git lfs fetch
git lfs checkout

酷酷的進度條就出現啦!不過步驟比較多,筆者還是習慣 git clone 就好,除非懷疑網路速度出了問題,不然通常不太需要這麼做。

Progress

有些模型可能不只包含 PyTorch 格式的權重,還會把 Tensorflow 與 JAX 格式的權重都上傳上來,這時用 git clone 的話,會複製三四倍大小的檔案下來,需要自行權衡一下是否需要這樣做。

大部分模型權重上傳之後就不會再變動了,但是 Git 的機制會讓下載下來的專案包含一份額外複製的權重做備份,因此 10 GB 的 Repo 複製到本機上,會吃掉 20 GB 的硬碟空間。一般情況下可以直接把權重資料夾裡面的 .git 資料夾刪掉以節省硬碟空間,有需要的話再下載回來就好。

讀取 Tokenizer

Tokenizer 的讀取方式,與模型讀取方式差不多,通常 Tokenizer 會跟模型權重的專案放在一起,所以直接用相同的 Namespace 下載即可:

from transformers import LlamaTokenizer as TkCls

tk: TkCls = TkCls.from_pretrained("TheBloke/Llama-2-7b-chat-fp16")

也可以使用類似 LlamaTokenizerFast 的快速類別,一般版的 Tokenizer 使用 Python 實做,而快速版的則是使用 Rust 實做。如果要對大量長文本進行分詞的話,可以考慮使用快速版,但如果只是要處理幾個短文本的話就不需要。

在舊版本的 Transformers 裡面,有些快速版的 Tokenizer 要初始化非常久。解決方法除了改用一般版以外,也可以將 Tokenizer 讀出來之後,轉存成快速版:

from transformers import LlamaTokenizerFast as TkCls

tk: TkCls = TkCls.from_pretrained("/path/to/tokenizer")
tk.save_pretrained("/path/to/tokenizer")

這樣下次讀取的時候就不會再等這麼久了,不過這個問題在最新版的 Transformers 已經被修正了,所以通常不用太擔心。

自動類別 Auto Class

因為我們已經知道 Llama 2 使用的是 Llama 架構,所以可以直接調用 LlamaForCausalLMLlamaTokenizer 這兩個類別。但有些模型與 Tokenizer 第一時間可能無法判別使用什麼架構,或者想撰寫相容性更廣泛的程式碼,那就可以使用 Transformers 的 Auto Class 來進行讀取:

from transformers import AutoModelForCausalLM as ModelCls
from transformers import AutoTokenizer as TkCls

model_path = "TheBloke/Llama-2-7b-chat-fp16"
model: ModelCls = ModelCls.from_pretrained(model_path)
tk: TkCls = TkCls.from_pretrained(model_path)

實際印出類別,還是原本的 Llama 類別:

print(type(model))  # <class 'transformers...LlamaForCausalLM'>
print(type(tk))     # <class 'transformers...LlamaTokenizerFast'>

如果不想用快速版的 Tokenizer 可以把 use_fast 參數設成 False:

model_path = "Llama-2-7b-chat-fp16"
tk: TkCls = TkCls.from_pretrained(model_path, use_fast=False)
print(type(tk))  # <class 'transformers...LlamaTokenizer'>

但 Auto Class 的缺點是沒有 Type Hint,像是在寫程式時 VSCode 會自己跳出來的自動完成提示清單:

Type Hint

如果非常仰賴 Type Hint 的開發者,可能要自己做 Type Annotation 來解決。

另外筆者一般而言會用 Alias 的方式縮短類別名稱:

from transformers import AutoModelForCausalLM as ModelCls
from transformers import AutoTokenizer as TkCls

以上是基本的讀取方法,接下來介紹如何實際使用 LLM 進行文本生成。

文本生成

在進行文本生成之前,我們需要先將輸入切成 Token 並轉換成 PyTorch Tensor 物件:

tk: TkCls = TkCls.from_pretrained("Llama-2-7b-chat-fp16")
tokens = tk("Hello, ", return_tensors="pt")
print(tokens)
"""
Output:
{
    "input_ids": tensor([[1, 15043, 29892, 29871]]),
    "attention_mask": tensor([[1, 1, 1, 1]]),
}
"""

Tokenize 的結果包含了 input_idsattention_mask 兩個結果,不過在推論時,我們只需要使用 input_ids 就好:

input_ids = tokens["input_ids"].to("cuda")
print(input_ids)
# tensor([[    1, 15043, 29892, 29871]], device='cuda:0')

接著就可以開始使用 model.generate 進行文本生成:

output = model.generate(input_ids, max_new_tokens=32)
print(tk.batch_decode(output))
# <s> Hello, I am a beginner in Python ...

model.generate 會回傳生成的 Token ID,必須用 Tokenizer 進行 Decode 才能得到文字版的輸出。max_new_tokens 是用來設定要輸出的 Token 數量,在 Transformer Decoder 裡面,輸出的 Token 數越多,佔用的 GPU 記憶體就會越多,生成所需的時間理所當然的也會比較久。

model.generate 也可以一次做很多個文本生成,例如:

tk.pad_token = tk.eos_token  # LlamaTokenizer 沒有 Padding Token
prompt = ["Hello, ", "Hi, my name is"]
tokens = tk(prompt, return_tensors="pt", padding=True)
input_ids = tokens["input_ids"].to("cuda")

outputs = model.generate(input_ids, max_new_tokens=16)
print(tk.batch_decode(outputs))

要特別注意,因為 Llama Tokenizer 沒有預設的對齊用 Token,所以需要幫他指定一個,才能做對齊。這時我們很開心的執行這份程式,並觀察輸出的結果:

[
    "<s> Hello, </s></s>0000000000000000",
    "<s> Hi, my name is [Your Name] and I am a [Your Profession] ...",
]

第一個結果好像怪怪的欸 🤔

那是因為 Transformers Decoder 是 Autoregressive 的關係,所以要把 Padding Token 放在左邊,我們可以在初始化 Tokenizer 時設定 padding_side 這個參數:

tk: TkCls = TkCls.from_pretrained(model_path, padding_side="left")

再執行一次上述的程式碼,結果就正常多了 🎉

串流輸出 Streaming

單純只用 Generate 的時候,通常都要等上一段時間,才會看到完整的輸出。當我們在調測 LLM 時,其實看中間的輸出就可以知道有沒有出問題。這時我們可以透過 TextStreamer 來進行串流輸出,使用方法非常簡單,只要用 Tokenizer 初始化一個 TextStreamer 物件之後當成參數丟進 model.generate 裡面就好了:

from transformers import TextStreamer

output = model.generate(
    input_ids,
    max_new_tokens=2048,
    streamer=TextStreamer(tk),
)

這樣就會看到文字一個一個跳出來啦!

提示樣板 Prompt Template

使用 LLM 進行文本生成,只使用 "Hello, " 之類的輸入是不夠的。若要發動 LLM 的對話能力,我們需要借助提示樣本的機制。提示樣本其實就是把使用者與模型之間一問一答的過程,寫成一個像是聊天紀錄的格式,用來「欺騙」語言模型生成下個聊天片段。

這個樣板會根據每個語言模型訓練的方式而有所不同,一般而言遵守模型開發者的建議會比較好。但除非該模型真的被訓練到很 Over Fitting 的狀態,不然我們只需要一個有點樣子的樣板就可以用了,例如:

### USER: 什麼是大型語言模型?
### ASSISTANT:

將這個 Prompt 輸入到模型裡面,讓模型幫我們完成接下來的段落:

prompt = "### USER: 什麼是大型語言模型?\n### ASSISTANT:"

tokens = tk(prompt, return_tensors="pt")
input_ids = tokens["input_ids"].to("cuda")

output = model.generate(
    input_ids,
    max_new_tokens=2048,
    streamer=TextStreamer(tk),
)

"""
輸出結果:
<s> ### USER: 什麼是大型語言模型?
### ASSISTANT: 大型語言模型(Large Language Model,LLM)是一種 ...
"""

雖然這並不符合 Llama 2 的規範,但一樣能有對話能力。

設定停止點 Stopped Words

在 Transformers 裡面要設定停止點會相對複雜一些,停止點是用來告訴系統在生成的過程中,除了遇到 EOS (End-of-Sentence) Token 以外,還有遇到哪些 Token 應該停止輸出。為了達成這個效果,我們必須自己實做 StoppingCriteria 類別:

from transformers import StoppingCriteria, StoppingCriteriaList


class StopWords(StoppingCriteria):
    def __init__(self, tk: TkCls, stop_words: list[str]):
        self.tk = tk
        self.stop_tokens = stop_words

    def __call__(self, input_ids, *_) -> bool:
        s = self.tk.batch_decode(input_ids)[0]
        for t in self.stop_tokens:
            if s.endswith(t):
                return True
        return False


sw = StopWords(tk, ["。", "!", "?"])
scl = StoppingCriteriaList([sw])

我們寫了一個 StopWords 類別,會在每次系統檢查的時候,將整份輸出 Decode 回文字,並且檢查文字的結尾是否符合使用者設定的停止點。這裡我們只實做單一輸入進行文本生成的情況,當有多個文本在進行生成時,情況會複雜許多。

接著將 StopWords 物件放進一個 StoppingCriteriaList 裡面,然後傳入 model.generate 裡面即可:

output = model.generate(
    input_ids,
    max_new_tokens=2048,
    streamer=TextStreamer(tk),
    stopping_criteria=scl,
)

這樣模型在輸出遇到 "。!?" 時就會停下來了。

取樣參數 Sample Parameters

模型在進行推論的時候,其實會生成一張機率表,用來代表下一個 Token 可能會是誰。我們可以在 model.generate 裡面指定一些取樣參數,讓模型不會每次都挑機率最高的那個 Token 當輸出,這樣每次輸出都會長的不太一樣,有時也會影響模型的回答是否正確,以下介紹一些常見的參數。

do_sample 用來控制是否進行取樣,若為 True 則代表要進行取樣,若為 False 則是 Greedy Decode。當使用 Greedy Decode 時,建議不要再設定其他取樣參數,否則還是有可能會影響模型的輸出。

top_k 代表要從機率表裡面取前幾名做取樣。top_p 稍微複雜,這個參數會設定一個浮點數,這時程式就會從第一名開始往後累加,把前幾名的機率全部加起來,直到這個累加的機率超過 top_p 為止,並從這些 Tokens 中取樣。

temperature 則是一種機率表的改造,類似以前學校常聽到的「開根號除以十」的概念,原本機率較低的 Token 會被調高,原本機率較高的 Token 會被調低。當 temperature 越大時,第一名跟最後一名的距離就會越近,但彼此之間的相對關係還是保持不變。temperature 經常被用來設定模型要「更有創意」還是「更加嚴謹」的生成輸出。

repetition_penalty 顧名思義就是在懲罰重複的輸出,當這個設定高於 1.0 時,模型較不容易產生重複性的輸出。低於 1.0 時,模型會變得容易生成重複的輸出。當你發現模型開始瘋狂跳針時,是個滿實用的參數。

最後我們的 model.generate 可能會長得像這樣:

model.generate(
    input_ids,
    max_new_tokens=16,
    streamer=ts,
    do_sample=True,
    top_k=50,
    top_p=0.95,
    temperature=0.75,
    repetition_penalty=1.1,
)

這些參數也會對彼此之間產生不同程度的影響,因此選擇一組適合的參數進行生成任務,也是一堂相當重要的課題。Transformers 可以用來控制生成的參數還有很多,詳細資訊可以參考官方文件

自迴歸解碼 Autoregressive Decoding

LLM 的自迴歸 (Autoregressive, AR) 簡單來講,就是說模型會一直生下一個 Token,直到生成結束為止。相對於非自迴歸 (Non-Autogressive, NAR) 模型而言,其差別在於自迴歸模型會把自己的輸出當成輸入,而 NAR 模型會一次把整個序列生出來。

在 LLM 裡面,每次推論 (Inference) 只會生成一個 Token,必須要經過多次推論,才能完成完整的文本生成 (Generation)。接下來,我們就來拆解 model.generate 的背後原理。

一般而言,我們可以直接透過 model() 來進行推論:

import torch

tokens = tk(prompt, return_tensors="pt")
input_ids = tokens["input_ids"].to("cuda")
with torch.no_grad():
    output = model(input_ids)

這裡會產生一個類別為 CausalLMOutputWithPast 的輸出,裡面有兩個重要的資訊,分別為 logitspast_key_values

logits 是模型預測下個 Token 可能是什麼的機率分佈,我們可以對這個 logits 做取樣,或者直接 argmax 做 Greedy Decode。

past_key_values 就是俗稱的 KV Cache,用來代表 Decoder LM 對已知輸入的運算結果。因為自迴歸的特性,所以 Decoder LM 每次生成的輸出都會變成下個回合的輸入,因此這個 KV Cache 會越長越大,同時也是消耗 GPU 記憶體的元兇之一。

有了這份 KV Cache,我們就不用每次推論時都把整個 input_ids 丟進去,只要留新長出來的 Token 就好,因為舊的輸入都已經被模型 Decode 成 KV Cache 了。因此我們下個回合的推論就會長這樣:

with torch.no_grad():
    outputs = model(
        outputs.logits.argmax(-1)[:, -1:],  # Greedy Decode
        past_key_values=output.past_key_values,
    )

我們將此邏輯整理成一個迴圈來進行:

prompt = "### USER: 什麼是大型語言模型?\n### ASSISTANT:"
tokens = tk(prompt, return_tensors="pt")
curr_input_ids = tokens["input_ids"].to("cuda")
pkv = None

output_tokens = list()
for i in range(16):
    with torch.no_grad():
        outputs = model(curr_input_ids, past_key_values=pkv)
    next_token = outputs.logits.argmax(-1)[:, -1:]
    token_id = next_token.detach().cpu().numpy()[0][0]

    if token_id == tk.eos_token_id:
        break

    output_tokens.append(token_id)
    curr_input_ids = next_token
    pkv = outputs.past_key_values

    probs = torch.softmax(outputs.logits, -1)
    token_prob = probs[-1][-1][token_id].item()

print(tk.decode(output_tokens))
# 輸出結果:大型語言模型(Large Language Model,LLM)

這邊我們固定進行 16 次推論,中間如果遇到 EOS Token 就會跳出迴圈。拆解生成步驟的好處在於,我們可以針對 logits 的機率分佈做觀察。有些時候我們可以看到模型在「說謊」時,那個部份的機率分佈會特別的低,可以由此來偵測模型的說法是否可信之類的。

多顯卡推論 Multi-GPUs

當機器有多顆 GPUs 時,可以透過環境變數 CUDA_VISIBLE_DEVICES 來指定要使用哪顆 GPU 進行運算:

# 指定使用 1 號 GPU 進行運算
CUDA_VISIBLE_DEVICES=1 python main.py

也可以透過 Python 的 os.environ 來指定:

import os

os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"

import transformers

在指定環境變數時,一定要先於匯入 Transformers 套件,否則容易失效。

使用多張 GPUs 同時進行推論的程式碼與一般推論大致相同:

import os

# 使用 0 號與 1 號 GPU 同時進行推論
os.environ["CUDA_VISIBLE_DEVICES"] = "0,1"

from transformers import LlamaForCausalLM as ModelCls
from transformers import LlamaTokenizer as TkCls
from transformers import TextStreamer

model_path = "TheBloke/Llama-2-7b-chat-fp16"
model: ModelCls = ModelCls.from_pretrained(
    model_path,
    device_map="auto"
)
tk: TkCls = TkCls.from_pretrained(model_path)
ts = TextStreamer(tk)

prompt = "Hello, "
input_ids = tk(prompt, return_tensors="pt")["input_ids"].to("cuda")
model.generate(input_ids, max_new_tokens=16, streamer=ts)

Candle

同樣由 Hugging Face 開發的 Candle 框架,是使用 Rust 語言開發的機器學習框架。Rust 是以高效率、高記憶體安全聞名的程式語言,是個閃亮的明日之星,因此 Candle 框架也是個相當值得關注的專案。

Candle Llama 2 Demo

結論

今天介紹了我們偉大的開源笑臉 Hugging Face Transformers 套件,並著重在推論方面進行講解。此套件的方便與靈活,讓我們能夠更輕鬆的在這些大大小小的模型之間穿梭。他們充滿活力的團隊與活躍的社群,讓整個開源社群更加蓬勃發展。如果想要更瞭解 Hugging Face,可以關注他們的部落格,閱讀他們豐富的文件,還有許多教學資源可以參考。

然而 Transformers 終究是個為了 Traning 而生的框架,其主要的功能還是在訓練模型上。因此在推論這塊,並不是最佳首選的框架。未來我們將會介紹各種專為 Inference 進行優化的框架,在此之前我們會先來探索這半年來,如雨後春筍般誕生的大大小小各式各樣的大型語言模型,那我們明天見啦!

參考


上一篇
LLM Note Day 10 - 建立開發環境
下一篇
LLM Note Day 12 - So Many LLMs 如繁星般的語言模型們
系列文
LLM 學習筆記33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言